const http = require('http');
const fs = require('fs');
const url = require('url');
const path = require('path');
const zlib = require('zlib');

const WebSocket = require('ws');

const { FrameUrl, info, errorInfo, travelDir } = require('./utils.js');

/**
 * @typedef ServerParams
 * @property {String} guard guard必须是baseDirInFileSystem的最后一层目录
 * @property {number} port
 * @property {String} baseDirInFileSystem
 * @property {function} proxy
 */


const contentTypeMap = {
    ".css": "text/css",
    ".gif": "image/gif",
    ".html": "text/html;charset=UTF-8",
    ".ico": "image/x-icon",
    ".jpeg": "image/jpeg",
    ".jpg": "image/jpeg",
    ".js": "application/javascript",
    ".json": "application/json",
    ".pdf": "application/pdf",
    ".png": "image/png",
    ".svg": "image/svg+xml",
    ".swf": "application/x-shockwave-flash",
    ".tiff": "image/tiff",
    ".txt": "text/plain",
    ".wav": "audio/x-wav",
    ".wma": "audio/x-ms-wma",
    ".wmv": "video/x-ms-wmv",
    ".xml": "text/xml"
};

/**
 * open static http server
 * todo:这个有一定安全问题,应该限制访问ip为127.0.0.1,防止远程访问,还应该加一个动态随机token,防止本机浏览器访问
 * @param {ServerParams} params
 */
function launchStaticServer(params) {
    /**
     * 注意这个是插件所在的文件路径
     */
    const baseDirInFileSystem = params.baseDirInFileSystem;
    /**
     * 对一个小项目而言,将资源完全缓冲起来也是没问题的
     */
    const bufferMap = {};
    travelDir(baseDirInFileSystem, file => {
        let data = fs.readFileSync(file);
        if (file.endsWith('.zip')) {
            data = zlib.gunzipSync(data);
            file = file.substring(0, file.length - 4);
        }
        bufferMap[file] = data;
    });

    /**
     * @todo 做一个验证,url必须是对127.0.0.1的访问,防止漏洞
     */
    const server = http.createServer(async (request, response) => {
        const url0 = url.parse(request.url);

        const pathName = path.normalize(url0.pathname);
        /**
         * 去掉前面的guard,也就是guard需要是文件目录真正的一层
         * 得到相对于resources/webview的相对路径
         */
        let requestPath = path.relative('/' + params.guard, pathName);
        /**
         * pathName开头不在params.guard(webview)下面,relative会在前面加上..\
         */
        if (requestPath.startsWith('..')) {
            /**
             * 添加proxy功能,这已经不是静态服务器了:)
             */
            if (params.proxy) {
                /**
                 * 允许跨域
                 */
                const headers = { 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' };
                const extname = path.extname(pathName), contentType = contentTypeMap[extname];
                contentType && (headers["Content-Type"] = contentType);
                try {
                    /**
                     * @type {Buffer}
                     */
                    res = await params.proxy(pathName);
                    if (res) {
                        response.writeHead(200, headers);
                        response.end(res);
                        return;
                    }
                } catch (e) {
                    errorInfo(e);
                    response.writeHead(500, headers);
                    response.end();
                    return;
                }
            }

            response.writeHead(401);
            response.end('401 Unauthorized');
        } else {
            //注意这里实际上又把guard给加上了
            let filePath = path.join(baseDirInFileSystem, requestPath);
            /**
             * {@type Buffer}
             */
            const data = bufferMap[filePath];
            if (data) {
                const extname = path.extname(requestPath);
                const contentType = contentTypeMap[extname];
                if (contentType) {
                    const headers = { "Content-Type": contentType, 'Cache-Control': 'max-age=1' };
                    if (extname === '.html') {
                        headers['X-Frame-Options'] = 'ALLOWALL';
                    }
                    response.writeHead(200, headers);
                } else {
                    response.writeHead(200);
                }
                response.end(data);
            } else {
                response.writeHead(404);
                response.end('404 Not Found');
            }
        }
    });

    server.listen(params.port);

    info('server start!');

    return server;
}

function undef(v) {
    return typeof v === 'undefined' || v === null;
}

function deserialize(str) {
    if (undef(str)) return null;
    str = str.trim();
    if (str.length === 0) return null;
    return JSON.parse(str);
}


/**
 * 对于每一个connection,都会创建一个WSListener对象
 */
class ConnectionListener {
    /** 
     * @param {WebSocket} ws websocket connection
     * @param serviceHandler 
     * 注册服务方法
     * serviceName => service function的映射.
     * 在被调时,框架接受serviceName和参数数组,在参数数组后面加入一个回调方法,在使用这个参数数组调用注册的service function
     * 回调方法为的内容为将回调方法接受到的参数+callId,回传给调用端
     */
    constructor(ws, serviceHandler) {
        this.ws = ws;
        this.serviceHandler = serviceHandler;
    }

    /**
     * 发送消息
     * @param {String} msg 
     * @param {number} socketId
     */
    send(msg, socketId) {
        if (this.ws != null) {
            try {
                this.ws.send(msg);
            } catch (e) {
                this.connectionClose(socketId);
                errorInfo('broken pipe!');
            }
        } else {
            errorInfo('can\'t send because connection closed!');
        }
    }

    /**
     * 接受消息
     * @param {number} socketId
     * @param mxDev
     *     ws客户端建立连接的时候带过来的参数
     * @param {String} msg 
     */
    onmessage(socketId, mxDev, msg) {
        const data = deserialize(msg);

        const __targetId__ = data['__targetId__'];
        if (__targetId__ === 'callService') {

            const { service: serviceName, callId, args } = data;
            info('web ide call service',serviceName);
            let service = this.serviceHandler[serviceName];
            if (service) {
                const me = this;
                //添加一个参数,为服务端响应客户端的方法,服务端可以多次响应客户端
                const reply = function () {
                    let res = [...arguments];
                    !res.length && (res = null);
                    const str = JSON.stringify({ res, callId });
                    me.send(str, socketId);
                };
                //将socketId绑定到最后一个参数上
                reply['__socketId'] = socketId;
                mxDev && (reply['__mxDev'] = mxDev);

                args.push(reply);
                try {
                    service.apply(null, args);
                } catch (e) {
                    errorInfo(e);
                }
            } else {
                errorInfo(serviceName + '并不存在!');
            }
        } else {
            throw new Error(`unsupport ${__targetId__}`);
        }
    }//~onmessage

    /**
     * connection连接关闭的时候调用
     */
    connectionClose(sockedId) {
        const ws = ConnectionListener.socketMap[sockedId];
        if (ws) {
            delete ConnectionListener.socketMap[sockedId];
            try {
                ws.terminate();
            } catch (e) {
                errorInfo(e);
            }

            function _fire(handler) {
                if (handler && handler.onconnectionclose) {
                    try {
                        handler.onconnectionclose(sockedId);
                    } catch (e) {
                        errorInfo(e);
                    }
                }
            }

            _fire(this.serviceHandler);
        }
    }

    static _socketSeq = 0;
    static nextSocketId() {
        return ConnectionListener._socketSeq++;
    }

    /**
     * socketId=>ws
     */
    static socketMap = {};
}

/**
 * 在http server基础上创建ws server
 * @param  {String} guard
 * @param  {*} httpServer
 * @param  {*} serviceHandlers
 * @return {*}
 */
function bindWSServer(guard, httpServer, serviceHandlers) {
    if (!guard.startsWith("/")) guard = '/' + guard;
    const server = new WebSocket.Server({ path: guard, server: httpServer });

    server.on('connection', function (socket, requestMsg) {
        /**
         * 取建立ws时,客户端带过来的参数
         */
        const url = requestMsg.url;
        let mxDev = new FrameUrl(url).getParam('mxDev');
        //发送端应该escape
        if (mxDev) {
            mxDev = unescape(mxDev);
            //mxDev参数应该是个json
            mxDev = eval('(' + mxDev + ')');
        }

        const socketId = ConnectionListener.nextSocketId();
        ConnectionListener.socketMap[socketId] = socket;
        /**
         * 对于每一个connection.都有一个唯一的listener
         */
        const listener = new ConnectionListener(socket, serviceHandlers);
        socket.on('message', function (msg) {
            // @ts-ignore
            listener.onmessage(socketId, mxDev, msg);
        });

        socket.on('close', function () {
            listener.connectionClose(socketId);
        });
    });

    info('websocket server bind!');

}



module.exports = { launchStaticServer, bindWSServer };